package uk.co.q3c.deplan.client.domain;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import javax.persistence.Transient;

import uk.co.q3c.deplan.client.domain.dag.ChangeInvalidException;
import uk.co.q3c.deplan.client.domain.dag.CycleDetectedException;
import uk.co.q3c.deplan.client.domain.resource.AbstractResource;
import uk.co.q3c.deplan.client.domain.resource.PersistenceReferenceException;
import uk.co.q3c.deplan.client.domain.resource.ResourcePool;
import uk.co.q3c.deplan.client.domain.task.BaseTask;
import uk.co.q3c.deplan.client.domain.task.PlanProfileEntry;
import uk.co.q3c.deplan.client.domain.util.NamedObject;
import uk.co.q3c.deplan.client.service.ServiceFactory;

import com.googlecode.objectify.Key;
import com.googlecode.objectify.annotation.Cached;
import com.googlecode.objectify.annotation.Indexed;
import com.googlecode.objectify.annotation.Unindexed;

/**
 * @see BaseTask
 * @see AbstractResource
 * @see ResourcePool
 * @see PlanProfileEntry
 * @author David Sowerby
 * 
 */

@Unindexed
@Cached(expirationSeconds = 600)
public abstract class AbstractDomainObject implements DomainObject, NamedObject, Serializable {
	private static final long serialVersionUID = 1L;

	@Transient
	protected transient ServiceFactory connection;

	@Transient
	transient List<PropertyChangeListener> nameChangeListeners;
	@Transient
	transient List<PropertyChangeListener> propertyChangeListeners;
	@Transient
	transient List<StructureChangeListener> structureChangeListeners;
	@Transient
	transient protected boolean initialised = false;
	@Indexed
	protected boolean discarded = false;

	protected Date discardedDate = null;
	@Transient
	transient boolean valid = false;

	private String name;

	/**
	 * 
	 */
	protected AbstractDomainObject() {
		super();
		init();
	}

	/**
	 * This constructor is not called by the persistence layer when an object is loaded, so an explicit call to
	 * {@link #afterLoad(ServiceFactory)} is made - the assumption is that this is a new object, not yet persisted, so {@link #init()} is
	 * called, which in turn calls {@link #beforeLoad()}, and then {@link #afterLoad(ServiceFactory)} is called to complete initialisation
	 * 
	 * @param dao
	 */
	public AbstractDomainObject(ServiceFactory connection) {
		super();
		this.connection = connection;
		init();
		afterLoad(connection);
	}

	@Override
	public void afterLoad(ServiceFactory connection) {
		this.connection = connection;
	}

	@Override
	public void setName(String s) {

		String oldValue = this.name;
		name = s;
		firePropertyChange("name", oldValue, this.name);
	}

	@Override
	public String getName() {
		if (name == null) {
			return "unnamed";
		}
		return name;
	}

	@Override
	public void addNameChangeListener(PropertyChangeListener listener) {
		if (nameChangeListeners == null) {
			nameChangeListeners = new ArrayList<PropertyChangeListener>();
		}
		nameChangeListeners.add(listener);

	}

	@Override
	public void removeNameChangeListener(PropertyChangeListener listener) {
		if (nameChangeListeners != null) {
			nameChangeListeners.remove(listener);
		}
		return;
	}

	public void copyPropertyChangeListeners(AbstractDomainObject source) {
		if (nameChangeListeners == null) {
			nameChangeListeners = new ArrayList<PropertyChangeListener>();
		}
		if (propertyChangeListeners == null) {
			propertyChangeListeners = new ArrayList<PropertyChangeListener>();
		}
		nameChangeListeners.addAll(source.nameChangeListeners);
		propertyChangeListeners.addAll(source.propertyChangeListeners);
	}

	public void addPropertyChangeListener(PropertyChangeListener listener) {
		if (propertyChangeListeners == null) {
			propertyChangeListeners = new ArrayList<PropertyChangeListener>();
		}
		propertyChangeListeners.add(listener);
	}

	public void removePropertyChangeListener(PropertyChangeListener listener) {
		if (propertyChangeListeners != null) {
			propertyChangeListeners.remove(listener);
		}
	}

	protected void firePropertyChange(String propertyName, Object oldValue, Object newValue) {
		PropertyChangeEvent evt = new PropertyChangeEvent(this, propertyName, oldValue, newValue);
		for (PropertyChangeListener listener : propertyChangeListeners) {
			listener.propertyChange(evt);
		}
	}

	protected void fireStructureChange(StructureChangeNotifier child, boolean added) {
		for (StructureChangeListener listener : structureChangeListeners) {
			listener.childChanged(child, added);
		}
	}

	public void addStructureChangeListener(StructureChangeListener listener) {
		if (structureChangeListeners == null) {
			structureChangeListeners = new ArrayList<StructureChangeListener>();
		}
		structureChangeListeners.add(listener);
	}

	public void removeStructureChangeListener(StructureChangeListener listener) {
		if (structureChangeListeners != null) {
			structureChangeListeners.remove(listener);
		}
	}

	/**
	 * 
	 * Subclasses should not override this method, but can override the associated {@link #beforeLoad()} method, which will be invoked only
	 * if initialisation hasn't already occurred. <br>
	 * 
	 * @see uk.co.q3c.deplan.domain.PersistedObject#init()
	 */
	public void init() {
		if (!initialised) {

			propertyChangeListeners = new ArrayList<PropertyChangeListener>();
			structureChangeListeners = new ArrayList<StructureChangeListener>();
			// for subclasses
			beforeLoad();
			// Model.registerPersistentObject(this);
			initialised = true;
		}
	}

	/**
	 * Subclasses should implement this method to carry out any initialisation needed after loading from persistence.
	 */
	protected void beforeLoad() {
	};

	public boolean isInitialised() {
		return initialised;
	}

	public boolean getDiscarded() {

		return discarded;
	}

	/**
	 * Sets the value of discarded, and if true, calls {@link #dereference()} to remove any references to this object. The discarded date is
	 * set to 'now', which means it reflects the last change to the status of discarded
	 * 
	 * @param discarded
	 * @throws CycleDetectedException
	 * @throws ChangeInvalidException
	 * @throws PersistenceReferenceException
	 */
	public void setDiscarded(boolean discarded) throws CycleDetectedException, ChangeInvalidException, PersistenceReferenceException {

		if (this.discarded != discarded) {
			boolean oldValue = this.discarded;
			this.discarded = discarded;
			if (discarded) {
				setDiscardedDate(new Date());
				dereference();
			} else {
				setDiscardedDate(null);
			}
			firePropertyChange("discarded", oldValue, this.discarded);
		}
	}

	/**
	 * Returns the date of the last change to {@link #discarded}
	 */
	public Date getDiscardedDate() {

		return discardedDate;
	}

	/**
	 * sets the date of the last change to {@link #discarded}
	 */
	public void setDiscardedDate(Date discardedDate) {

		if (this.discardedDate == null) {
			this.discardedDate = new Date();
		}
		Date oldValue = new Date(this.discardedDate.getTime());
		if (discardedDate == null) {
			this.discardedDate = null;
		} else {
			this.discardedDate.setTime(discardedDate.getTime());
		}
		firePropertyChange("discardedDate", oldValue, this.discardedDate);
	}

	/**
	 * Does nothing in this base class, override where needed
	 * 
	 * @throws CycleDetectedException
	 * @throws ChangeInvalidException
	 * @throws PersistenceReferenceException
	 * 
	 * @see uk.co.q3c.deplan.domain.PersistedObject#dereference()
	 */
	@Override
	public void dereference() throws CycleDetectedException, ChangeInvalidException, PersistenceReferenceException {

	}

	public boolean isValid() {
		return valid;
	}

	public void setValid(boolean valid) {
		this.valid = valid;
	}

	@Override
	public ServiceFactory getConnection() {
		return connection;
	}

	protected Key<? extends DomainObject> updateKey(DomainObject reference) throws PersistenceReferenceException {
		if (reference == null) {
			return null;
		}
		return reference.getKey();
	}

	@Override
	public void setConnection(ServiceFactory connection) {
		this.connection = connection;
	}

	@Override
	public String getCounterGroup() {
		return "other";
	}

	/**
	 * Refreshes the keys from the provided list of references. If references is null, the call is ignored. (it is expected this would
	 * happen if references, being transient, hadn't been populated in the first place)
	 * 
	 * @param <T>
	 * @param references
	 * @param keys
	 */
	@SuppressWarnings("unchecked")
	protected <E extends DomainObject> void syncKeys(List<E> references, List<Key<E>> keys) {
		if (references == null) {
			return;
		}
		keys.clear();
		for (DomainObject ref : references) {
			Key<E> k = new Key(ref.getClass(), ref.getKeyId());
			keys.add(k);
		}
	}

	protected void prePersist() {
	}

	/**
	 * Uses resource service as a generic save, a bit misleading, fix one day
	 */
	@Override
	public void save() {
		connection.resourceService()
				.save(this);

	}

}
